package cc.verywell.pureblock.msg.nfs.idx.extraUpload;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import cc.verywell.pureblock.func.node.KeyShell;
import cc.verywell.pureblock.log.Pblog;
import cc.verywell.pureblock.msg.nfs.hd.LfsHdSupply;
import cc.verywell.pureblock.msg.nfs.hd.NoEnoughSpaceException;
import cc.verywell.pureblock.msg.nfs.idx.NfsidxStructure;
import cc.verywell.pureblock.msg.nfs.strc.NfsStructureV1;
import cc.verywell.pureblock.msg.nfs.strc.NfsStructureV1.DataLink;
import cc.verywell.pureblock.msg.nfs.strc.NfsStructureV1.Sign;
import cc.verywell.pureblock.msg.nfs.strc.NfsStructureV1.Sub;
import cc.verywell.pureblock.msg.nfs.strc.NfsStructureV1encoder;
import cc.verywell.pureblock.net.AioProvider;
import cc.verywell.pureblock.net.NormalSession;
import cc.verywell.pureblock.util.StringUtil;
import cc.verywell.pureblock.util.sign.Sha;

/**
* 
* Copyright © 2019 TanYaqiu. All rights reserved.
* @author TanYaqiu 
* @address flat 601
* @date 2019年10月2日 下午6:03:35
*/
public class UploadManager implements UploadSupply {

	UploadNeed out;
	public UploadManager(UploadNeed out) {
		this.out = out;
	}
	
	private Map<String,TopKeyHold> lockmap = new ConcurrentHashMap<>();
	private Map<KeyShell,UploadingWatch> uploadingWatch = new ConcurrentHashMap<>();
	private Map<UploadingMonitor,String> uploadingMonitor = new ConcurrentHashMap<>();
	@Override
	public String toString() {
		return "UploadManager"+" locks:"+lockmap.size()+" watchs:"+uploadingWatch.size()+" monitor:"+uploadingMonitor.size();
	}
	
	public enum UPLOAD{
		OK,
		ERR,
		UPLOADING,
		UPLOADCANCELED,
		FILENOTEXIST,
		FILECANNOTLOCK,
		NOENOUGHSPACE;
	}
	public static class UploadCallback{
		final public UPLOAD backcode;
		final public UploadingMonitor monitor;
		public UploadCallback(UPLOAD backcode, UploadingMonitor monitor) {
			this.backcode = backcode;
			this.monitor = monitor;
		}
	}
	public static class TopKeyHold{
		public UploadingMonitor k;
	}
	public UploadCallback upload(Path filePath, List<Sub> alias, List<Sign> signs, List<DataLink> extralinks) {
		String absolutePath = filePath.toAbsolutePath().toString();
		TopKeyHold lock = new TopKeyHold(),oldlock;
		UploadingMonitor m = null;
		for(;;)
		synchronized (lock) {
			oldlock = lockmap.putIfAbsent(absolutePath,lock);
			if( null == oldlock ) {
				long totalsize = -1;
				boolean ok = false;
				try(
						RandomAccessFile fileaccess = new RandomAccessFile(filePath.toFile(), "rw");
						FileChannel filechannel = fileaccess.getChannel();
						FileLock filelock = filechannel.tryLock(0,filechannel.size(),true);
						) {
					;
					if(filelock==null)return new UploadCallback(UPLOAD.FILECANNOTLOCK,m);
					////开始分析文件;
					totalsize = filechannel.size();
					NfsStructureV1encoder V1enc = new NfsStructureV1encoder(totalsize, alias, signs, extralinks);
					byte[] aPbs = StringUtil.stringToByte(absolutePath);
					m = new UploadingMonitor(this, aPbs, true);
					KeyShell mainKey = convertNfsStructureV1encoder(ByteBuffer.allocate((int)Math.min(AioProvider.FILE_MAX_SIZE,totalsize+NfsStructureV1.STRUCTURE_HEAD_SIZE)), V1enc, m, aPbs, filechannel);
					m.mainKey = mainKey;
					lock.k = m;
					////结束文件分析;
					ok = true;
					return new UploadCallback(UPLOAD.OK,m);
				} catch (FileNotFoundException e) {
					m.close();
					return new UploadCallback(UPLOAD.FILENOTEXIST,m);
				} catch (NoEnoughSpaceException e) {
					m.close();
					if(Pblog.FS) {
						long a = 0;
						if(alias!=null)
							for (Sub s : alias)
								a += s.encodeSize();
						if(signs!=null)
							for (Sign s : signs)
								a += s.encodeSize();
						if(extralinks!=null)
							for (DataLink s : extralinks)
								a += s.encodeSize();
						Pblog.FS("extraUpload ",filePath," ",LfsHdSupply.formatSize(totalsize),"+",LfsHdSupply.formatSize(a)," ",e);
					}
					return new UploadCallback(UPLOAD.NOENOUGHSPACE,m);
				} catch (IOException e) {
					e.printStackTrace();
				}finally {
					lockmap.remove(absolutePath, lock); //这里
					if(ok) {
						uploadingMonitor.put(m, absolutePath);
						m.fireUploadReadybyManager();
					}else {
						if(m!=null)m.close();
					}
				}
				break;
			}else {
				synchronized (oldlock) {
					//等待上一个文件分析结束
				}
				///TODO 遇到退出要停止循环
			}
		}
		if(oldlock!=null) {
			return new UploadCallback(UPLOAD.UPLOADING,oldlock.k);
		}
		//if(m!=null)m.close();
		return new UploadCallback(UPLOAD.ERR,m);
	}
	private void uploadwatch(KeyShell key,UploadingMonitor m,NfsidxStructure idx) {
		final UploadingWatch watch = new UploadingWatch(key);
		UploadingWatch newwatch = uploadingWatch.compute(key, (k,old)->{
			if(old==null) {
				watch.addMonitor(m, idx);
				return watch;
			}
			synchronized (old) {
				if(old.disabled()) {
					watch.addMonitor(m, idx);
					return watch;
				}else {
					old.addMonitor(m, idx);
					return old;
				}
			}
		});
		if(newwatch==watch) {
			out.addIndex(key, idx);
		}else if(!idx.isExternal()){
			out.addIndex(key, idx);
		}
	}
	private KeyShell convertNfsStructureV1encoder(ByteBuffer btemp,NfsStructureV1encoder V1enc, UploadingMonitor m, byte[] absolutePath,FileChannel filechannel) throws IOException {
		if(V1enc.extra==null) {
			NfsidxStructure idx =
					(V1enc.offset==0 && filechannel.size()==V1enc.length)
					?new NfsidxStructure(true, true, absolutePath, V1enc.length)
					:new NfsidxStructure(true, true, absolutePath, V1enc.offset, V1enc.length);
			//ByteBuffer btemp = ByteBuffer.allocate(NfsStructureV1.STRUCTURE_HEAD_SIZE+(int)V1enc.length);
			btemp.clear();
			btemp.limit(NfsStructureV1.STRUCTURE_HEAD_SIZE+(int)V1enc.length);
			btemp.put((byte)0);
			if(V1enc.length!=filechannel.read(btemp,V1enc.offset)) {
				throw new IOException("File:"+StringUtil.byteToString(absolutePath)+" offset:"+V1enc.offset+" length:"+V1enc.length+" read err");
			}
			btemp.flip();
			byte[] keyhash = Sha.getInstance().hash(btemp);
			KeyShell key = new KeyShell(keyhash);
			uploadwatch(key, m, idx);
			return key;
		}else{
			ArrayList<NfsStructureV1.DataLink> datakeys;
			if(V1enc.extra.datalinks!=null) {
				datakeys = new  ArrayList<NfsStructureV1.DataLink>(V1enc.extra.datalinks.size());
				for (NfsStructureV1encoder ve : V1enc.extra.datalinks) {
					long size = ve.length;
					if(ve.extra!=null)size += ve.extra.datasize;
					KeyShell key = convertNfsStructureV1encoder(btemp, ve, m, absolutePath, filechannel);
					datakeys.add(new NfsStructureV1.DataLinkWrite(key.key, size));
				}
			}else {
				datakeys = null;
			}
			int size = NfsStructureV1.encodeSize((int)V1enc.length, datakeys, V1enc.extra.alias, V1enc.extra.signs, V1enc.extra.extralinks);
			if(size>AioProvider.FILE_MAX_SIZE) {
				System.err.println("data:"+V1enc.length);			
				System.err.println("datas:"+(datakeys==null?0:datakeys.size()));			
				System.err.println("alias:"+(V1enc.extra.alias==null?0:V1enc.extra.alias.size()));			
				System.err.println("signs:"+(V1enc.extra.signs==null?0:V1enc.extra.signs.size()));			
				System.err.println("extra:"+(V1enc.extra.extralinks==null?0:V1enc.extra.extralinks.size()));			
				throw new IOException("NfsStructureV1 file:"+StringUtil.byteToString(absolutePath)+" size too big : "+size+" limit "+AioProvider.FILE_MAX_SIZE);
			}
			byte[] strc = new byte[size];
			ByteBuffer bb = ByteBuffer.wrap(strc);
			NfsStructureV1.encode(bb, null, datakeys, V1enc.extra.alias, V1enc.extra.signs, V1enc.extra.extralinks, (byte)0);
			if(V1enc.length!=bb.remaining()) {
				throw new IOException("NfsStructureV1 file:"+StringUtil.byteToString(absolutePath)+" strcRemain:"+bb.remaining()+"/"+bb.capacity()+" != length:"+V1enc.length+" offset:"+V1enc.offset);
			}
			if(V1enc.length!=filechannel.read(bb,V1enc.offset)) {
				throw new IOException("File:"+StringUtil.byteToString(absolutePath)+" offset:"+V1enc.offset+" length:"+V1enc.length+" read err");
			}
			byte[] keyhash = Sha.getInstance().hash(strc);
			KeyShell key = new KeyShell(keyhash);
			NfsidxStructure idx = out.putAndHoldInternal(m,key,strc);
			uploadwatch(key, m, idx);
			return key;
		}
	}
	
	void removeWatchbyUploadingMonitor(UploadingWatch w) {
		synchronized (w) {
			uploadingWatch.computeIfPresent(w.key, (k,old)->{
				if(old==w && w.needDisable())return null;
				return old;
			});
		}
	}
	void removeMonitorbyUploadingMonitor(UploadingMonitor m) {
		String absolutePath = uploadingMonitor.remove(m);
		if(absolutePath!=null) {
			TopKeyHold h =  lockmap.computeIfPresent(absolutePath, (k,old)->{
				if(old.k==m)return null;
				return old;
			});
			if(h==null) {
				//TODO 告知监听器
			}
		}
	}
	
	@Override
	public NfsidxStructure needNewExternalFile(KeyShell key, NfsidxStructure errFile) {
		UploadingWatch w = uploadingWatch.get(key);
		if(w==null)return null;
		return w.needNewExternalFile(key, errFile);
	}
	@Override
	public void fireStoreFile(NormalSession nc, List<byte[]> fileHash) {
		if(uploadingWatch.isEmpty())return;
		for (byte[] keyhash : fileHash) {
			UploadingWatch w = uploadingWatch.get(new KeyShell(keyhash));
			if(w!=null) {
				w.fireStoreFile(nc, fileHash);
			}
		}
	}
	@Override
	public void fireWantFile(NormalSession nc, byte[] fileHash) {
		UploadingWatch w = uploadingWatch.get(new KeyShell(fileHash));
		if(w!=null) {
			w.fireWantFile(nc, fileHash);
		}
	}
	
	////20200204 为DownloadManager提供上传支持
	/**
	 * 新可做种数据包
	 * @param key
	 * @param m
	 * @param absolutePath
	 * @param totalsize
	 * @param pos
	 * @param len
	 */
	void newDataReadyByUploadingMonitor(KeyShell key,UploadingMonitor m, byte[] absolutePath, long totalsize, long pos, long len) {
		NfsidxStructure idx =
				(pos==0 && totalsize==len)
				?new NfsidxStructure(true, true, absolutePath, totalsize)
				:new NfsidxStructure(true, true, absolutePath, pos, len);
		uploadwatch(key, m, idx);
	}
	void tryHoldInternal(UploadingMonitor m,KeyShell key,byte[] value) {
		NfsidxStructure idx = out.tryHoldInternal(m,key,value);
	}
	/**
	 * download取得uploadMonitor, 可以在uploadMonitor.newDataReady为已准备好的数据做种
	 * @param absolutePath
	 * @return
	 */
	public UploadingMonitor getUploadMonitor(String absolutePath) {
		UploadingMonitor m = null;
		try {
			byte[] aPbs = StringUtil.stringToByte(absolutePath);
			m = new UploadingMonitor(this, aPbs, false);
		}finally {
			if(m!=null) {
				uploadingMonitor.put(m, absolutePath);
				m.fireUploadReadybyManager();
			}
		}
		return m;
	}
	
}
